import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as net from 'net';
import * as path from 'path';
import * as fs from 'fs';
import { randomUUID } from 'crypto';
import { promisify } from 'util';
import { dumpBackendOutput, recordBackendOutput } from '../utils/backendLogs';
import { BackendMethodDescriptor, Attachment, GenerationStatusPayload } from '../types/types';

const PROJECT_STATE_KEY = 'testagent.projectPath';
const DEFAULT_BACKEND_PORT = 8848;

interface ProgressEvent {
    seq?: number;
    type: string;
    node?: string;
    tool?: string;
    status?: string;
    message?: string;
    detail?: string;
    requestId?: string;
    prompt?: string;
    context?: string;
}

interface ProgressResponse {
    events?: ProgressEvent[];
    finished?: boolean;
    latestSeq?: number;
    missing?: boolean;
}

interface TestAgentConfig {
    neo4j: {
        serverUrl: string;
        serverUser: string;
        serverPassword: string;
    };
    llm: {
        apiBase: string;
        apiKey: string;
        model: string;
    };
    environment: {
        JAVA_HOME: string;
    };
}

const execAsync = promisify(cp.exec);

export class TestAgentService {
    private backendProcess: cp.ChildProcess | undefined;
    private backendStartupPromise: Promise<boolean> | undefined;
    private readonly backendProcessGroupSupported = process.platform !== 'win32';
    private readonly methodCache = new Map<number, BackendMethodDescriptor>();
    private readonly outputChannel: vscode.OutputChannel;

    constructor(private readonly context: vscode.ExtensionContext) {
        this.outputChannel = vscode.window.createOutputChannel('TestAgent');
    }

    public dispose(): void {
        this.terminateBackendProcess();
        this.outputChannel.dispose();
    }

    public getSelectedProjectPath(): string | undefined {
        return this.context.workspaceState.get<string>(PROJECT_STATE_KEY);
    }

    public async selectProject(): Promise<string | undefined> {
        const folder = await vscode.window.showOpenDialog({
            canSelectFolders: true,
            openLabel: 'Select Project Folder'
        });

        if (!folder || folder.length === 0) {
            vscode.window.showErrorMessage('❌ No project folder selected.');
            return undefined;
        }

        const selectedUri = folder[0];
        const projectPath = selectedUri.fsPath;
        await this.context.workspaceState.update(PROJECT_STATE_KEY, projectPath);

        vscode.window.showInformationMessage(`📁 Selected project: ${projectPath}`);

        try {
            await vscode.commands.executeCommand('vscode.openFolder', selectedUri, false);
        } catch (err: any) {
            this.outputChannel.appendLine(`⚠️ Failed to open folder automatically: ${err?.message ?? String(err)}`);
            vscode.window.showWarningMessage(
                `⚠️ Project path saved, but failed to open folder automatically: ${err?.message ?? String(err)}`
            );
        }

        return projectPath;
    }

    public async generateKnowledgeGraph(): Promise<boolean> {
        const projectPath = this.getSelectedProjectPath();
        if (!projectPath) {
            vscode.window.showErrorMessage('❌ Please select a project before generating the knowledge graph.');
            return false;
        }

        let generationSucceeded = false;
        await vscode.window.withProgress(
            {
                location: vscode.ProgressLocation.Notification,
                title: '🔄 Generating knowledge graph...'
            },
            async () => {
                const neo4jReady = await this.ensureNeo4jRunning();
                if (!neo4jReady) {
                    vscode.window.showErrorMessage('❌ Neo4j is not running and could not be started. Aborting generation.');
                    return;
                }
                generationSucceeded = await this.executeJar(projectPath);
            }
        );

        if (!generationSucceeded) {
            this.outputChannel.appendLine('⚠️ Knowledge graph generation did not complete successfully.');
        }

        return generationSucceeded;
    }

    public async openUserConfigFile(message?: string): Promise<void> {
        const configPath = this.ensureConfigFile();
        const document = await vscode.workspace.openTextDocument(vscode.Uri.file(configPath));
        await vscode.window.showTextDocument(document, { preview: false });
        if (message) {
            vscode.window.showInformationMessage(message);
        }
    }

    public async ensureUserConfigReady(): Promise<boolean> {
        const { ready, missing } = this.validateUserConfig();
        if (!ready) {
            await this.openUserConfigFile();
            const missingText = missing.length > 0 ? missing.join('、') : '必要的环境配置';
            vscode.window.showWarningMessage(`请先在 core/config/userConfig.json 中配置：${missingText}`);
            return false;
        }
        return true;
    }

    public async fetchMethodList(): Promise<BackendMethodDescriptor[]> {
        const ready = await this.ensureBackendRunning();
        if (!ready) {
            throw new Error('Backend service is not available.');
        }
        const config = this.getConfig();
        const params = new URLSearchParams({
            limit: '100',
            offset: '0',
            focalOnly: 'true',
            url: config.neo4j.serverUrl || '',
            username: config.neo4j.serverUser || '',
            password: config.neo4j.serverPassword || ''
        });
        const res = await fetch(`http://127.0.0.1:8848/listMethods?${params.toString()}`);
        const text = await res.text();
        if (!res.ok) {
            throw new Error(`Backend responded ${res.status}: ${text}`);
        }

        let payload: any;
        try {
            payload = JSON.parse(text);
        } catch (err) {
            throw new Error(`Unexpected backend response: ${text}`);
        }

        if (payload.result !== 'Success') {
            throw new Error(payload.message ?? 'Backend returned failure.');
        }

        const methods: BackendMethodDescriptor[] = (payload.methods ?? []).map((method: any) => ({
            id: method.id,
            name: method.name,
            className: method.class_name,
            packageName: method.package_name,
            signature: method.signature
        }));

        this.methodCache.clear();
        methods.forEach(m => this.methodCache.set(m.id, m));

        return methods;
    }

    public async generateTests(
        methodIds: number[],
        assistMode: 'auto' | 'human' = 'auto',
        onStatus?: (status: GenerationStatusPayload) => void
    ): Promise<void> {
        if (!methodIds || methodIds.length === 0) {
            vscode.window.showWarningMessage('Please select at least one method.');
            return;
        }

        const ready = await this.ensureBackendRunning();
        if (!ready) {
            vscode.window.showErrorMessage('❌ Backend service is not available.');
            return;
        }

        const allowHumanAssistance = assistMode === 'human';
        const modeLabel = allowHumanAssistance ? '人类介入' : '自主完成';
        this.outputChannel.appendLine(`🧪 Generating tests...（模式：${modeLabel}）`);
        this.outputChannel.show(true);

        await vscode.window.withProgress(
            {
                location: vscode.ProgressLocation.Notification,
                title: '🧪 Generating tests...'
            },
            async progress => {
                for (const methodId of methodIds) {
                    const method = this.methodCache.get(methodId);
                    const label = this.getMethodLabel(methodId, method);
                    progress.report({ message: `Generating test for ${label}` });
                    onStatus?.({ level: 'info', message: `▶️ Generating test for ${label}` });
                    this.outputChannel.appendLine(`▶️ Generating test for ${label}`);

                    const jobId = this.createJobId();
                    const progressController = new AbortController();
                    const seenSeqs = new Set<number>();
                    const monitorPromise = this.monitorProgress(
                        jobId,
                        progressController.signal,
                        label,
                        seenSeqs,
                        onStatus
                    );

                    const dumpBackendSnapshot = this.createBackendSnapshotDumper(label);

                    try {
                        const payload = await this.requestGeneration(methodId, jobId, allowHumanAssistance);
                        await this.handleGenerationResponse(payload, label, onStatus);
                    } catch (err: any) {
                        const message = err?.message ?? String(err);
                        this.outputChannel.appendLine(`❌ Error generating ${label}: ${message}`);
                        onStatus?.({ level: 'error', message: `❌ Error generating ${label}: ${message}` });
                        dumpBackendSnapshot('generateTest request');
                    } finally {
                        const abortTimeout = setTimeout(() => progressController.abort(), 5000);
                        try {
                            const monitorResult = await monitorPromise;
                            await this.flushProgressHistory(jobId, seenSeqs, label, monitorResult.supported, onStatus);
                        } catch (err: any) {
                            dumpBackendSnapshot('progress log flush');
                        } finally {
                            clearTimeout(abortTimeout);
                            progressController.abort();
                        }
                    }
                }
            }
        );
    }

    private async requestGeneration(methodId: number, jobId: string, allowHumanAssistance: boolean): Promise<any> {
        const config = this.getConfig();
        const payload = {
            methodId,
            jobId,
            allowHumanAssistance,
            url: config.neo4j.serverUrl,
            username: config.neo4j.serverUser,
            password: config.neo4j.serverPassword,
            projectPath: this.getSelectedProjectPath()
        };
        const res = await fetch('http://127.0.0.1:8848/generateTest', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
        const responseText = await res.text();
        if (!res.ok) {
            throw new Error(`Backend responded ${res.status}: ${responseText}`);
        }
        try {
            return JSON.parse(responseText);
        } catch (err) {
            throw new Error(`Unexpected backend response: ${responseText}`);
        }
    }

    private async handleGenerationResponse(
        payload: any,
        label: string,
        onStatus?: (status: GenerationStatusPayload) => void
    ): Promise<void> {
        if (payload.result === 'Success') {
            const savedFiles = this.extractSavedFiles(payload.output);
            if (savedFiles.length > 0) {
                await this.openFileIfExists(savedFiles[0]);
                if (savedFiles.length > 1) {
                    onStatus?.({
                        level: 'info',
                        message: `📁 ${savedFiles.length} 个测试文件已全部生成。`
                    });
                }
                onStatus?.({ level: 'success', message: `✅ Generated tests for ${label}: ${savedFiles[0]}` });
                const attachments = this.createFileAttachments(savedFiles);
                if (attachments.length) {
                    onStatus?.({
                        level: 'info',
                        message: `🗂️ 生成的测试文件（${label}）`,
                        files: attachments
                    });
                }
            } else {
                const testCode = (payload.output?.test_code ?? []).join('\n\n');
                const doc = await vscode.workspace.openTextDocument({
                    content: testCode,
                    language: 'java'
                });
                await vscode.window.showTextDocument(doc, { preview: false });
                onStatus?.({ level: 'success', message: `✅ Generated tests for ${label}` });
            }
            this.publishReportSummaries(payload.output, label, onStatus);
            this.outputChannel.appendLine(`✅ Generated tests for ${label}`);
        } else {
            const errorMsg = payload.message ?? 'Generation failed';
            this.outputChannel.appendLine(`❌ Generation failed for ${label}: ${errorMsg}`);
            onStatus?.({ level: 'error', message: `❌ Generation failed for ${label}: ${errorMsg}` });
        }
    }

    private extractSavedFiles(output: any): string[] {
        if (!output) {
            return [];
        }
        if (Array.isArray(output.saved_files)) {
            return output.saved_files.filter((file: unknown) => typeof file === 'string' && file.length > 0) as string[];
        }
        if (typeof output.saved_file === 'string' && output.saved_file.length > 0) {
            return [output.saved_file];
        }
        return [];
    }

    private async openFileIfExists(filePath: string | undefined): Promise<void> {
        if (!filePath) {
            return;
        }
        if (fs.existsSync(filePath)) {
            const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath));
            await vscode.window.showTextDocument(doc, { preview: false });
        }
    }

    private createJobId(): string {
        try {
            return randomUUID();
        } catch {
            return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
        }
    }

    private getMethodLabel(methodId: number, method?: BackendMethodDescriptor): string {
        if (!method) {
            return `Method#${methodId}`;
        }
        return `${method.className ?? 'UnknownClass'}.${method.name ?? 'method'}`;
    }

    private async ensureBackendRunning(port = DEFAULT_BACKEND_PORT): Promise<boolean> {
        this.outputChannel.appendLine('🔎 Checking backend status...');
        const healthyProcess = this.backendProcess && this.backendProcess.exitCode === null && !this.backendProcess.killed
            ? this.backendProcess
            : undefined;

        if (healthyProcess) {
            const alive = await this.waitForHealthCheck(port, 5000);
            if (alive) {
                this.outputChannel.appendLine('✅ FastAPI backend is ready.');
                return true;
            }
            this.outputChannel.appendLine('⚠️ Backend process detected but /health not ready, restarting...');
        }

        if (this.backendStartupPromise) {
            this.outputChannel.appendLine('⏳ Backend startup in progress, waiting...');
            return this.backendStartupPromise;
        }

        this.backendStartupPromise = (async () => {
            const alreadyAlive = await this.waitForHealthCheck(port, 3000, true);
            if (alreadyAlive) {
                vscode.window.showInformationMessage('✅ TestAgent backend is already running.');
                return true;
            }

            const portBusy = await this.isPortInUse(port);
            if (portBusy) {
                vscode.window.showErrorMessage(`❌ Port ${port} is in use. Please free the port and retry.`);
                this.outputChannel.appendLine(`❌ Port ${port} is busy.`);
                return false;
            }

            const neo4jReady = await this.ensureNeo4jRunning();
            if (!neo4jReady) {
                this.outputChannel.appendLine('❌ Failed to start backend because Neo4j is not running.');
                return false;
            }

            vscode.window.showInformationMessage('🧩 Starting TestAgent backend service...');
            this.backendProcess = this.startPythonBackend(port);

            const alive = await this.waitForHealthCheck(port, 15000);
            if (alive) {
                vscode.window.showInformationMessage('✅ TestAgent backend is ready.');
                this.outputChannel.appendLine('✅ FastAPI backend is ready.');
                return true;
            }

            vscode.window.showErrorMessage('❌ Failed to start TestAgent backend.');
            this.outputChannel.appendLine('❌ Backend /health not reachable, startup failed.');
            return false;
        })();

        try {
            return await this.backendStartupPromise;
        } finally {
            this.backendStartupPromise = undefined;
        }
    }

    private startPythonBackend(port = DEFAULT_BACKEND_PORT): cp.ChildProcess | undefined {
        try {
            const serverPath = path.join(this.context.extensionPath, 'core', 'server', 'stubs', 'testAgentServer.py');
            const config = this.getConfig();
            const runtimeInfo = this.prepareBundledPythonEnv();
            const baseEnv = runtimeInfo?.env ?? process.env;
            const pythonEnv = {
                ...baseEnv,
                NEO4J_URL: config.neo4j?.serverUrl || baseEnv.NEO4J_URL,
                NEO4J_USER: config.neo4j?.serverUser || baseEnv.NEO4J_USER,
                NEO4J_PASSWORD: config.neo4j?.serverPassword || baseEnv.NEO4J_PASSWORD,
            };

            let pythonCmd: string;
            let args: string[];

            if (runtimeInfo) {
                pythonCmd = runtimeInfo.pythonPath;
                args = [serverPath];
                this.outputChannel.appendLine(`🔧 Starting TestAgent backend with bundled Python: ${runtimeInfo.pythonPath}`);
            } else {
                pythonCmd = 'conda';
                args = ['run', '-n', 'TestAgent', 'python', serverPath];
                this.outputChannel.appendLine(`🔧 Starting TestAgent backend via conda env: ${serverPath}`);
            }

            const proc = cp.spawn(pythonCmd, args, {
                cwd: this.context.extensionPath,
                env: pythonEnv,
                detached: this.backendProcessGroupSupported
            });

            proc.stdout.on('data', data => {
                const text = data.toString();
                recordBackendOutput(text, 'stdout');
            });

            proc.stderr.on('data', data => {
                const text = data.toString();
                recordBackendOutput(text, 'stderr');
            });

            proc.on('exit', code => {
                recordBackendOutput(`process exit with code ${code ?? 'unknown'}`, 'meta');
                this.outputChannel.appendLine(`⚠️ Backend exited with code ${code}`);
                this.backendProcess = undefined;
            });

            proc.on('error', err => {
                recordBackendOutput(`process error: ${err.message}`, 'meta');
                vscode.window.showErrorMessage(`❌ Unable to start TestAgent backend: ${err.message}`);
                this.outputChannel.appendLine(`❌ Backend start error: ${err.message}`);
            });

            return proc;
        } catch (err: any) {
            vscode.window.showErrorMessage(`❌ Failed to start backend: ${err?.message ?? String(err)}`);
            this.outputChannel.appendLine(`❌ Backend start exception: ${err?.message ?? String(err)}`);
            return undefined;
        }
    }

    private prepareBundledPythonEnv():
        | { pythonPath: string; env: NodeJS.ProcessEnv }
        | undefined {
        try {
            const runtimeDir = path.join(this.context.extensionPath, 'runtime', 'python-env');
            const unpackedDir = path.join(runtimeDir, 'unpacked');
            const pythonCandidates = process.platform === 'win32'
                ? [
                    path.join(unpackedDir, 'python.exe'), // conda env on Windows usually places python.exe at root
                    path.join(unpackedDir, 'Scripts', 'python.exe')
                ]
                : [path.join(unpackedDir, 'bin', 'python')];

            if (!pythonCandidates.some(fs.existsSync)) {
                const archiveCandidates = this.getArchiveCandidates(runtimeDir);
                const archivePath = archiveCandidates.find(fs.existsSync);

                if (!archivePath) {
                    this.outputChannel.appendLine(`[runtime] Bundled Python archive not found. Tried: ${archiveCandidates.map(p => path.basename(p)).join(', ')}`);
                    return undefined;
                }

                // 清理旧的跨平台残留，避免解压后仍找不到 python.exe
                if (fs.existsSync(unpackedDir)) {
                    fs.rmSync(unpackedDir, { recursive: true, force: true });
                }
                fs.mkdirSync(unpackedDir, { recursive: true });

                this.outputChannel.appendLine(`[runtime] Using bundled Python archive: ${path.basename(archivePath)}`);
                const extractResult = cp.spawnSync('tar', ['-xzf', archivePath, '-C', unpackedDir]);
                if (extractResult.status !== 0) {
                    this.outputChannel.appendLine(`[runtime] Failed to extract Python archive: ${extractResult.stderr?.toString() ?? ''}`);
                    return undefined;
                }
                if (!pythonCandidates.some(fs.existsSync)) {
                    this.outputChannel.appendLine(`[runtime] Extracted archive but missing interpreter. Tried: ${pythonCandidates.join(', ')}`);
                    return undefined;
                }
            }

            const pythonPath = pythonCandidates.find(fs.existsSync)!;

            const unpackScript = process.platform === 'win32'
                ? path.join(unpackedDir, 'Scripts', 'conda-unpack.exe')
                : path.join(unpackedDir, 'bin', 'conda-unpack');
            const unpackMarker = path.join(unpackedDir, '.conda_unpacked');
            if (fs.existsSync(unpackScript) && !fs.existsSync(unpackMarker)) {
                const unpackResult = cp.spawnSync(unpackScript, [], { cwd: unpackedDir });
                if (unpackResult.status === 0) {
                    fs.writeFileSync(unpackMarker, new Date().toISOString());
                } else {
                    this.outputChannel.appendLine(`[runtime] conda-unpack failed: ${unpackResult.stderr?.toString() ?? ''}`);
                }
            }

            const binDir = process.platform === 'win32'
                ? (fs.existsSync(path.join(unpackedDir, 'Scripts')) ? path.join(unpackedDir, 'Scripts') : unpackedDir)
                : path.join(unpackedDir, 'bin');

            // 保留系统 PATH，避免丢失 Maven 等可执行文件
            const originalPath = process.platform === 'win32'
                ? (process.env.PATH || process.env.Path || '')
                : (process.env.PATH || '');

            const env: NodeJS.ProcessEnv = {
                ...process.env,
                PYTHONHOME: unpackedDir,
            };
            env.PATH = binDir + path.delimiter + originalPath;
            if (process.platform === 'win32') {
                // 部分 Windows 环境变量使用 Path 区分大小写，双写一份以防止丢失
                env.Path = env.PATH;
            }
            return { pythonPath, env };
        } catch (err: any) {
            this.outputChannel.appendLine(`[runtime] Failed to prepare bundled Python: ${err?.message ?? String(err)}`);
            return undefined;
        }
    }

    /**
     * 返回当前平台优先的压缩包候选列表，最后一个是通用回退名。
     */
    private getArchiveCandidates(runtimeDir: string): string[] {
        const candidates: string[] = [];
        const { platform, arch } = process;

        if (platform === 'win32') {
            candidates.push(
                path.join(runtimeDir, 'TestAgent-conda-pack-win64.tar.gz'),
                path.join(runtimeDir, 'TestAgent-conda-pack-windows.tar.gz')
            );
        } else if (platform === 'darwin') {
            if (arch === 'arm64') {
                candidates.push(path.join(runtimeDir, 'TestAgent-conda-pack-osx-arm64.tar.gz'));
            } else {
                candidates.push(path.join(runtimeDir, 'TestAgent-conda-pack-osx-x64.tar.gz'));
            }
        } else {
            // Linux 等其他平台
            if (arch === 'arm64') {
                candidates.push(path.join(runtimeDir, 'TestAgent-conda-pack-linux-arm64.tar.gz'));
            } else {
                candidates.push(path.join(runtimeDir, 'TestAgent-conda-pack-linux-x64.tar.gz'));
            }
        }

        // 兼容旧命名
        candidates.push(path.join(runtimeDir, 'TestAgent-conda-pack.tar.gz'));
        return candidates;
    }

    private terminateBackendProcess(signal: NodeJS.Signals = 'SIGTERM'): void {
        if (!this.backendProcess) {
            return;
        }
        const pid = this.backendProcess.pid;
        this.outputChannel.appendLine('🛑 Stopping TestAgent backend process...');
        try {
            if (this.backendProcessGroupSupported && pid) {
                process.kill(-pid, signal);
            } else if (process.platform === 'win32' && pid) {
                cp.spawn('taskkill', ['/pid', `${pid}`, '/T', '/F']);
            } else {
                this.backendProcess.kill(signal);
            }
        } catch (err: any) {
            this.outputChannel.appendLine(`⚠️ Failed to stop backend: ${err?.message ?? String(err)}`);
        } finally {
            this.backendProcess = undefined;
        }
    }

    private async waitForHealthCheck(port: number, timeout = 10000, silent = false): Promise<boolean> {
        const start = Date.now();
        const url = `http://127.0.0.1:${port}/health`;
        let warned = false;

        while (Date.now() - start < timeout) {
            try {
                const res = await this.fetchWithTimeout(url, {}, 2000);
                if (res.ok) {
                    const json: any = await res.json();
                    if (json.status === 'ok') {
                        if (!silent) {
                            this.outputChannel.appendLine('✅ /health check passed.');
                        }
                        return true;
                    }
                }
            } catch (e: any) {
                if (!silent && !warned) {
                    this.outputChannel.appendLine(`⏳ Waiting for /health: ${e?.message ?? e}`);
                    warned = true;
                }
            }
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
        if (!silent) {
            this.outputChannel.appendLine('⛔ /health check timed out.');
        }
        return false;
    }

    private async fetchWithTimeout(url: string, init: RequestInit = {}, timeoutMs = 5000): Promise<Response> {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        try {
            return await fetch(url, { ...init, signal: controller.signal });
        } finally {
            clearTimeout(timeout);
        }
    }

    private async isPortInUse(port: number, host = '127.0.0.1', timeoutMs = 1000): Promise<boolean> {
        return new Promise(resolve => {
            const socket = net.createConnection({ port, host });
            const finalize = (inUse: boolean) => {
                socket.destroy();
                resolve(inUse);
            };
            socket.setTimeout(timeoutMs, () => finalize(false));
            socket.once('connect', () => finalize(true));
            socket.once('error', (err: NodeJS.ErrnoException) => {
                if (err.code === 'ECONNREFUSED' || err.code === 'EHOSTUNREACH') {
                    finalize(false);
                } else {
                    finalize(true);
                }
            });
        });
    }

    private async ensureNeo4jRunning(): Promise<boolean> {
        const statusResult = await this.runNeo4jCli('status');
        const outputText = `${statusResult.stdout}\n${statusResult.stderr}`.trim();
        if (outputText.includes('Neo4j is running')) {
            return true;
        }
        if (statusResult.error && (statusResult.error as NodeJS.ErrnoException).code === 'ENOENT') {
            vscode.window.showErrorMessage(
                '❌ Unable to find the "neo4j" CLI. Please ensure Neo4j is installed and on PATH.'
            );
            return false;
        }
        vscode.window.showInformationMessage('ℹ️ Neo4j is not running. Attempting to start it now...');
        const startResult = await this.runNeo4jCli('start');
        const startOutput = `${startResult.stdout}\n${startResult.stderr}`.trim();
        if (startResult.ok && startOutput.includes('Neo4j is running')) {
            vscode.window.showInformationMessage('✅ Neo4j has been started.');
            return true;
        }
        if (!startResult.ok) {
            vscode.window.showErrorMessage(
                `❌ Failed to start Neo4j automatically.\n${startOutput || startResult.error?.message || ''}`
            );
            return false;
        }
        // vscode.window.showWarningMessage(
        //     '⚠️ Neo4j start command completed, but status was unclear. Please ensure Neo4j is running before retrying.'
        // );
        return true;
    }

    private async runNeo4jCli(command: 'status' | 'start'): Promise<{ ok: boolean; stdout: string; stderr: string; error?: Error & { code?: string } }> {
        try {
            const { stdout, stderr } = await execAsync(`neo4j ${command}`);
            return { ok: true, stdout, stderr };
        } catch (error: any) {
            return {
                ok: false,
                stdout: error?.stdout ?? '',
                stderr: error?.stderr ?? '',
                error
            };
        }
    }

    private async executeJar(projectPath: string): Promise<boolean> {
        const config = this.getConfig();
        const jarPath = path.join(this.context.extensionPath, 'core', 'CKGConstruction-1.0-SNAPSHOT.jar');
        if (!fs.existsSync(jarPath)) {
            vscode.window.showErrorMessage(
                '❌ Unable to locate the bundled CKGConstruction-1.0-SNAPSHOT.jar. Try reinstalling the extension.'
            );
            return false;
        }
        const neo4jUrl = config.neo4j.serverUrl;
        const neo4jUser = config.neo4j.serverUser;
        const neo4jPassword = config.neo4j.serverPassword;

        if (!neo4jUrl || !neo4jUser || !neo4jPassword) {
            vscode.window.showErrorMessage('Please update core/config/userConfig.json with the Neo4j settings.');
            return false;
        }

        const extensionLogDir = path.join(this.context.extensionPath, 'logs');
        if (!fs.existsSync(extensionLogDir)) {
            fs.mkdirSync(extensionLogDir, { recursive: true });
        }
        const logPath = path.join(extensionLogDir, 'ckg.log');
        const normalizedLogPath = path.resolve(logPath);
        const args = [
            `-Dlog.path=${normalizedLogPath}`,
            '-jar',
            jarPath,
            '-p',
            projectPath,
            '-u',
            neo4jUrl,
            '-n',
            neo4jUser,
            '-w',
            neo4jPassword
        ];

        const outputChannel = vscode.window.createOutputChannel('Knowledge Graph Generation');
        outputChannel.show(true);

        try {
            vscode.window.showInformationMessage('Preparing project and generating knowledge graph...');
            const mvnCommand = this.getMavenCommand();
            await this.runProcess(mvnCommand, ['clean', 'compile'], projectPath, outputChannel, 'mvn clean compile');
            await this.runProcess(
                mvnCommand,
                ['dependency:copy-dependencies'],
                projectPath,
                outputChannel,
                'mvn dependency:copy-dependencies'
            );
            await this.runProcess('java', args, undefined, outputChannel, 'Generate Knowledge Graph');
            vscode.window.showInformationMessage('Knowledge graph generated successfully!');
            return true;
        } catch (err: any) {
            vscode.window.showErrorMessage(`Failed to generate knowledge graph: ${err?.message ?? String(err)}`);
            outputChannel.appendLine(`❌ ${err?.message ?? String(err)}`);
            return false;
        }
    }

    private getMavenCommand(): string {
        return process.platform === 'win32' ? 'mvn.cmd' : 'mvn';
    }

    private runProcess(
        command: string,
        args: string[],
        cwd: string | undefined,
        outputChannel: vscode.OutputChannel,
        stepDescription: string
    ): Promise<void> {
        return new Promise((resolve, reject) => {
            outputChannel.appendLine(`\n[${stepDescription}] $ ${command} ${args.join(' ')}`);
            const proc = cp.spawn(command, args, {
                cwd,
                shell: process.platform === 'win32'
            });

            const recentLogs: string[] = [];
            const pushLogs = (chunk: string) => {
                const lines = chunk.replace(/\r/g, '').split('\n').map(line => line.trimEnd()).filter(Boolean);
                for (const line of lines) {
                    recentLogs.push(line);
                    if (recentLogs.length > 20) {
                        recentLogs.shift();
                    }
                }
            };

            proc.stdout.on('data', data => pushLogs(data.toString()));
            proc.stderr.on('data', data => pushLogs(data.toString()));

            proc.on('close', code => {
                if (code === 0) {
                    outputChannel.appendLine(`[${stepDescription}] completed successfully.`);
                    resolve();
                } else {
                    if (recentLogs.length) {
                        outputChannel.appendLine(`[${stepDescription}] recent output:`);
                        recentLogs.forEach(line => outputChannel.appendLine(line));
                    }
                    reject(new Error(`${stepDescription} failed with exit code ${code}`));
                }
            });

            proc.on('error', err => {
                if (recentLogs.length) {
                    outputChannel.appendLine(`[${stepDescription}] recent output:`);
                    recentLogs.forEach(line => outputChannel.appendLine(line));
                }
                reject(err);
            });
        });
    }

    private getConfig(): TestAgentConfig {
        const configPath = this.ensureConfigFile();
        const configData = fs.readFileSync(configPath, 'utf8');
        try {
            return JSON.parse(configData) as TestAgentConfig;
        } catch (err) {
            throw new Error('Failed to parse configuration file.');
        }
    }

    private ensureConfigFile(): string {
        const configDir = path.join(this.context.extensionPath, 'core', 'config');
        const configPath = path.join(configDir, 'userConfig.json');
        if (!fs.existsSync(configDir)) {
            fs.mkdirSync(configDir, { recursive: true });
        }
        if (!fs.existsSync(configPath)) {
            const defaultConfig: TestAgentConfig = {
                neo4j: {
                    serverUrl: 'bolt://localhost:7687',
                    serverUser: 'neo4j',
                    serverPassword: '123456'
                },
                llm: {
                    apiBase: '',
                    apiKey: '',
                    model: 'deepseek-chat'
                },
                environment: {
                    JAVA_HOME: ''
                }
            };
            fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
        }
        return configPath;
    }

    private validateUserConfig(): { ready: boolean; missing: string[] } {
        const config = this.getConfig();
        const missing: string[] = [];
        const hasValue = (value?: string) => typeof value === 'string' && value.trim().length > 0;

        if (!hasValue(config.llm?.apiBase)) {
            missing.push('LLM API Base');
        }
        if (!hasValue(config.llm?.apiKey)) {
            missing.push('LLM API Key');
        }
        if (!hasValue(config.neo4j?.serverUrl)) {
            missing.push('Neo4j Server URL');
        }
        if (!hasValue(config.neo4j?.serverUser)) {
            missing.push('Neo4j Username');
        }
        if (!hasValue(config.neo4j?.serverPassword)) {
            missing.push('Neo4j Password');
        }
        if (!hasValue(config.environment?.JAVA_HOME)) {
            missing.push('JAVA_HOME');
        }

        return {
            ready: missing.length === 0,
            missing
        };
    }

    private resolvePath(target: string): string {
        if (path.isAbsolute(target)) {
            return target;
        }
        return path.join(this.context.extensionPath, target);
    }

    private async monitorProgress(
        jobId: string,
        signal: AbortSignal,
        label: string,
        seenSeqs: Set<number>,
        onStatus?: (status: GenerationStatusPayload) => void
    ): Promise<{ supported: boolean }> {
        if (!jobId) {
            return { supported: true };
        }
        let after = 0;
        let supported = true;
        while (!signal.aborted) {
            try {
                const res = await fetch(`http://127.0.0.1:${DEFAULT_BACKEND_PORT}/progress/${jobId}?after=${after}`, { signal });
                if (res.status === 404) {
                    supported = false;
                    break;
                }
                if (!res.ok) {
                    throw new Error(`status ${res.status}`);
                }
                const payload = (await res.json()) as ProgressResponse;
                if (payload.events) {
                    for (const event of payload.events) {
                        after = Math.max(after, event.seq ?? after);
                        if (this.registerProgressEvent(event, seenSeqs)) {
                            if (event.type === 'human_assist_request') {
                                await this.handleHumanAssistanceRequest(event, label);
                            }
                            const formatted = this.formatProgressEvent(event, label);
                            if (formatted) {
                                onStatus?.({ level: 'info', message: formatted });
                                this.outputChannel.appendLine(formatted);
                            }
                        }
                    }
                }
                if (payload.finished && !payload.missing) {
                    break;
                }
                if (payload.missing) {
                    await this.waitWithSignal(500, signal);
                    continue;
                }
            } catch (err: any) {
                if (signal.aborted) {
                    break;
                }
                const errorMessage = err?.message ?? String(err);
                if (errorMessage?.toLowerCase().includes('fetch failed')) {
                    await this.waitWithSignal(1000, signal);
                    continue;
                }
                onStatus?.({ level: 'error', message: `⚠️ Progress polling error for ${label}: ${errorMessage}` });
            }
            await this.waitWithSignal(1000, signal);
        }
        return { supported };
    }

    private formatProgressEvent(event: ProgressEvent, label: string): string | undefined {
        switch (event.type) {
            case 'node':
            case 'tool':
                return undefined;
            case 'human_assist_request':
                return `🆘 [${label}] 请求人类协助：${event.prompt ?? event.message ?? ''}`;
            case 'status':
                return `ℹ️ [${label}] ${event.status ?? 'status'}${event.message ? `: ${event.message}` : ''}`;
            case 'info':
                return `ℹ️ [${label}] ${event.message}`;
            default:
                return undefined;
        }
    }

    private async flushProgressHistory(
        jobId: string,
        seenSeqs: Set<number>,
        label: string,
        progressSupported: boolean,
        onStatus?: (status: GenerationStatusPayload) => void
    ): Promise<void> {
        if (!jobId || !progressSupported) {
            return;
        }
        try {
            const res = await this.fetchWithTimeout(
                `http://127.0.0.1:${DEFAULT_BACKEND_PORT}/progress/${jobId}?after=0`,
                {},
                5000
            );
            if (!res.ok) {
                throw new Error(`status ${res.status}`);
            }
            const payload = (await res.json()) as ProgressResponse;
            if (payload.missing && !seenSeqs.size) {
                onStatus?.({ level: 'info', message: `ℹ️ [${label}] Backend did not report progress.` });
                return;
            }
            if (!payload.events || payload.events.length === 0) {
                if (!seenSeqs.size) {
                    onStatus?.({ level: 'info', message: `ℹ️ [${label}] Backend finished without events.` });
                }
                return;
            }
            for (const event of payload.events) {
                if (this.registerProgressEvent(event, seenSeqs)) {
                    if (event.type === 'human_assist_request') {
                        await this.handleHumanAssistanceRequest(event, label);
                    }
                    const formatted = this.formatProgressEvent(event, label);
                    if (formatted) {
                        onStatus?.({ level: 'info', message: formatted });
                        this.outputChannel.appendLine(formatted);
                    }
                }
            }
        } catch (err: any) {
            const errorMessage = err?.message ?? String(err);
            if (errorMessage?.toLowerCase().includes('fetch failed')) {
                return;
            }
            if (!seenSeqs.size) {
                onStatus?.({
                    level: 'error',
                    message: `⚠️ Unable to retrieve backend progress log for ${label}: ${errorMessage}`
                });
            }
        }
    }

    private waitWithSignal(ms: number, signal: AbortSignal): Promise<void> {
        return new Promise(resolve => {
            if (signal.aborted) {
                resolve();
                return;
            }
            const timeout = setTimeout(resolve, ms);
            signal.addEventListener(
                'abort',
                () => {
                    clearTimeout(timeout);
                    resolve();
                },
                { once: true }
            );
        });
    }

    private registerProgressEvent(event: ProgressEvent, seenSeqs: Set<number>): boolean {
        if (typeof event.seq === 'number') {
            if (seenSeqs.has(event.seq)) {
                return false;
            }
            seenSeqs.add(event.seq);
        }
        return true;
    }

    private async handleHumanAssistanceRequest(event: ProgressEvent, label: string): Promise<void> {
        if (!event.requestId) {
            return;
        }
        const prompt = event.prompt ?? event.message ?? '';
        const context = event.context ?? '';
        const detail = context ? `${prompt}\n\n上下文：${context}` : prompt;
        const response = await vscode.window.showInputBox({
            prompt: `【${label}】需要人类协助`,
            value: '',
            placeHolder: detail || '请输入要发送给后端的人类反馈，留空表示跳过',
            ignoreFocusOut: true
        });
        const content = response ?? '';
        try {
            await this.postAssistanceResponse(event.requestId, content);
            this.outputChannel.appendLine(
                content.trim()
                    ? `🧑‍💻 已向后端发送人类协助回复（${label}）`
                    : `🧑‍💻 人类协助未提供额外信息（${label}）`
            );
        } catch (err: any) {
            this.outputChannel.appendLine(
                `❌ 无法发送人类协助回复：${err?.message ?? String(err)}`
            );
        }
    }

    private async postAssistanceResponse(requestId: string, content: string): Promise<void> {
        const res = await fetch('http://127.0.0.1:8848/assistance/respond', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ requestId, content })
        });
        if (!res.ok) {
            const text = await res.text();
            throw new Error(`status ${res.status}: ${text}`);
        }
    }

    private createFileAttachments(filePaths: string[]): Attachment[] {
        return filePaths
            .filter(filePath => typeof filePath === 'string' && filePath.length > 0)
            .map(filePath => ({
                type: 'file',
                path: filePath,
                name: path.basename(filePath),
                relativePath: this.asRelativeWorkspacePath(filePath)
            }));
    }

    private publishReportSummaries(
        output: any,
        label: string,
        onStatus?: (status: GenerationStatusPayload) => void
    ): void {
        if (!output) {
            return;
        }
        const coverageMessage = this.buildCoverageMessage(output, label);
        if (coverageMessage) {
            onStatus?.({
                level: 'info',
                message: coverageMessage,
                metrics: { coverage: typeof output.final_coverage === 'number' ? output.final_coverage : undefined }
            });
        }
        const mutationMessage = this.buildMutationMessage(output, label);
        if (mutationMessage) {
            onStatus?.({
                level: 'info',
                message: mutationMessage,
                metrics: { mutation: typeof output.final_mutation_score === 'number' ? output.final_mutation_score : undefined }
            });
        }
    }

    private buildCoverageMessage(output: any, label: string): string | undefined {
        const finalCoverage = typeof output?.final_coverage === 'number'
            ? this.formatPercentage(output.final_coverage)
            : undefined;
        const bestLine = this.extractMaxPercentage(output?.line_coverage_rate);
        const bestBranch = this.extractMaxPercentage(output?.branch_coverage_rate);
        if (!finalCoverage && !bestLine && !bestBranch) {
            return undefined;
        }
        const details = [finalCoverage ? `final ${finalCoverage}` : undefined,
            bestLine ? `best line ${bestLine}` : undefined,
            bestBranch ? `best branch ${bestBranch}` : undefined]
            .filter(Boolean)
            .join(', ');
        return `📊 Coverage report for ${label}: ${details}`;
    }

    private buildMutationMessage(output: any, label: string): string | undefined {
        const finalScore = typeof output?.final_mutation_score === 'number'
            ? this.formatPercentage(output.final_mutation_score)
            : undefined;
        const bestScore = this.extractMaxPercentage(output?.mutation_score);
        if (!finalScore && !bestScore) {
            return undefined;
        }
        const details = [finalScore ? `final ${finalScore}` : undefined,
            bestScore && bestScore !== finalScore ? `best ${bestScore}` : undefined]
            .filter(Boolean)
            .join(', ');
        return `🧬 Mutation report for ${label}: ${details}`;
    }

    private extractMaxPercentage(values: unknown): string | undefined {
        if (!Array.isArray(values) || values.length === 0) {
            return undefined;
        }
        const numericValues = values
            .map(value => typeof value === 'number' ? value : Number(value))
            .filter(value => !Number.isNaN(value));
        if (!numericValues.length) {
            return undefined;
        }
        const maxValue = Math.max(...numericValues);
        return this.formatPercentage(maxValue);
    }

    private formatPercentage(value: number): string {
        let percentage = value;
        if (percentage <= 1) {
            percentage *= 100;
        }
        return `${percentage.toFixed(1)}%`;
    }

    private asRelativeWorkspacePath(filePath: string): string {
        try {
            const relative = vscode.workspace.asRelativePath(filePath, false);
            return relative && relative !== filePath ? relative : filePath;
        } catch {
            return filePath;
        }
    }

    private createBackendSnapshotDumper(label: string) {
        let dumped = false;
        return (reason?: string) => {
            if (dumped) {
                return;
            }
            dumped = true;
            const context = reason ? `${label} - ${reason}` : label;
            dumpBackendOutput(this.outputChannel, context);
        };
    }
}
